/************************************************************************
* shader.c
* voxelands - 3d voxel world sandbox game
* Copyright (C) Lisa 'darkrose' Milne 2016 <lisa@ltmnet.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program.  If not, see <http://www.gnu.org/licenses/>
************************************************************************/

#include "common.h"
#include "graphics.h"
#include "file.h"
#include "list.h"

#include <string.h>

static shader_t *shaders = NULL;
static shader_t *active = NULL;

static GLuint shader_load(file_t *f, GLenum type)
{
	GLint i;
	GLuint s = 0;

	if (!f)
		return s;

	s = glCreateShader(type);
	glShaderSource(s, 1, (const GLchar**)&f->data,NULL);
	glCompileShader(s);
	glGetShaderiv(s,GL_COMPILE_STATUS,&i);
	if (i == GL_FALSE) {
		char buff[512];
		glGetShaderInfoLog(s,512,&i,buff);
		vlprintf(CN_WARN, "Shader %s failed to compile: %s",f->name,buff);
		glDeleteShader(s);
		s = 0;
	}

	return s;
}

shader_t *shader_create(char* name)
{
	char buff[512];
	file_t *vert;
	file_t *geom;
	file_t *frag;
	shader_t *s;
	GLint i;

	if (snprintf(buff,512,"%s_vertex.glsl",name) < 512)
		vert = file_load("shader",buff);
	if (snprintf(buff,512,"%s_geometry.glsl",name) < 512)
		geom = file_load("shader",buff);
	if (snprintf(buff,512,"%s_fragment.glsl",name) < 512)
		frag = file_load("shader",buff);

	if (!vert && !geom && !frag)
		return NULL;

	s = malloc(sizeof(shader_t));
	if (!s) {
		if (vert)
			file_free(vert);
		if (geom)
			file_free(geom);
		if (frag)
			file_free(frag);
		return NULL;
	}

	strncpy(s->name,name,256);
	s->active = 0;
	s->program = 0;
	s->vert = 0;
	s->geom = 0;
	s->frag = 0;

	if (vert)
		s->vert = shader_load(vert,GL_VERTEX_SHADER);
	if (geom)
		s->geom = shader_load(geom,GL_GEOMETRY_SHADER);
	if (frag)
		s->frag = shader_load(frag,GL_FRAGMENT_SHADER);

	if (!s->vert && !s->geom && !s->frag) {
		if (vert)
			file_free(vert);
		if (geom)
			file_free(geom);
		if (frag)
			file_free(frag);
		free(s);
		return NULL;
	}

	s->program = glCreateProgram();
	if (s->vert)
		glAttachShader(s->program,s->vert);
	if (s->geom)
		glAttachShader(s->program,s->geom);
	if (s->frag)
		glAttachShader(s->program,s->frag);
	glLinkProgram(s->program);
	glGetProgramiv(s->program, GL_LINK_STATUS, &i);
	if (i == GL_FALSE) {
		glGetProgramInfoLog(s->program, 512, &i, buff);
		vlprintf(CN_INFO, "Shader failed to link: %s",buff);
	}

	glValidateProgram(s->program);

	if (vert)
		file_free(vert);
	if (geom)
		file_free(geom);
	if (frag)
		file_free(frag);

	shaders = list_push(&shaders,s);

	return s;
}

/* free a shader */
void shader_free(shader_t *s)
{
	if (!s)
		return;

	if (s->active)
		shader_disable(s);

	glDetachShader(s->program,s->vert);
	glDetachShader(s->program,s->frag);
	glDeleteShader(s->vert);
	glDeleteShader(s->frag);
	glDeleteProgram(s->program);

	free(s);
}

/* bind an attribute to a shader */
int shader_attribute(shader_t *s, int att, char* name)
{
	if (!s)
		return 1;

	glBindAttribLocation(s->program,att,name);
	glLinkProgram(s->program);
	glValidateProgram(s->program);

	return 0;
}

/* set a shader uniform value */
int shader_uniform_int(shader_t *s, char* name, int value)
{
	GLint var;
	if (!s)
		s = active;
	if (!s)
		return 1;

	var = glGetUniformLocation(s->program,name);
	if (var < 0)
		return 1;
	glUniform1i(var,value);

	return 0;
}

/* set a shader uniform value */
int shader_uniform_float(shader_t *s, char* name, float value)
{
	GLint var;
	if (!s)
		s = active;
	if (!s)
		return 1;

	var = glGetUniformLocation(s->program,name);
	if (var < 0)
		return 1;
	glUniform1f(var,value);

	return 0;
}

/* set a shader uniform value */
int shader_uniform_v2(shader_t *s, char* name, v2_t *value)
{
	GLint var;
	if (!s)
		s = active;
	if (!s)
		return 1;

	var = glGetUniformLocation(s->program,name);
	if (var < 0)
		return 1;
	glUniform2f(var,value->x,value->y);

	return 0;
}

/* set a shader uniform value */
int shader_uniform_v3(shader_t *s, char* name, v3_t *value)
{
	GLint var;
	if (!s)
		s = active;
	if (!s)
		return 1;

	var = glGetUniformLocation(s->program,name);
	if (var < 0)
		return 1;
	glUniform3f(var,value->x,value->y,value->z);

	return 0;
}

/* set a shader uniform value */
int shader_uniform_v4(shader_t *s, char* name, v4_t *value)
{
	GLint var;
	if (!s)
		s = active;
	if (!s)
		return 1;

	var = glGetUniformLocation(s->program,name);
	if (var < 0)
		return 1;

	glUniform4f(var,value->x,value->y,value->z,value->w);

	return 0;
}

/* set a shader uniform value */
int shader_uniform_colour(shader_t *s, char* name, colour_t *value)
{
	GLint var;
	GLfloat r;
	GLfloat g;
	GLfloat b;
	GLfloat a;
	if (!s)
		s = active;
	if (!s)
		return 1;

	var = glGetUniformLocation(s->program,name);
	if (var < 0)
		return 1;

	r = (float)value->r/255.0;
	g = (float)value->g/255.0;
	b = (float)value->b/255.0;
	a = (float)value->a/255.0;

	glUniform4f(var,r,g,b,a);

	return 0;
}

/* set a shader uniform value */
int shader_uniform_matrix(shader_t *s, char* name, matrix_t *value)
{
	GLint var;
	if (!s)
		s = active;
	if (!s)
		return 1;

	var = glGetUniformLocation(s->program,name);
	if (var < 0)
		return 1;

	glUniformMatrix4fv(var,1,GL_FALSE,value->data);

	return 0;
}

/* bind all 3 uniforms for a light */
int shader_uniform_light(shader_t *s, int slot, light_t *light)
{
	char buff[256];

	if (!light) {
		snprintf(buff,256,"light%d_active",slot);
		return shader_uniform_int(s,buff,0);
	}

	snprintf(buff,256,"light%d_active",slot);
	shader_uniform_int(s,buff,1);

	snprintf(buff,256,"light%d_pos",slot);
	shader_uniform_v3(s,buff,&light->pos);

	snprintf(buff,256,"light%d_att",slot);
	shader_uniform_v3(s,buff,&light->att);

	snprintf(buff,256,"light%d_colour",slot);
	shader_uniform_colour(s,buff,&light->colour);

	return 0;
}

/* use a shader */
int shader_enable(shader_t *s)
{
	if (!s || s->active)
		return 1;

	if (active)
		shader_disable(active);

	glUseProgram(s->program);
	s->active = 1;

	active = s;

	return 0;
}

/* stop using a shader */
int shader_disable(shader_t *s)
{
	if (!s || !s->active)
		return 1;

	active = NULL;

	glUseProgram(0);
	s->active = 0;

	return 0;
}
